/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Copyright (c) 2003-2009 Flamingo Kirill Grouchnikov
 * and <a href="http://www.topologi.com">Topologi</a>. 
 * Contributed by <b>Rick Jelliffe</b> of <b>Topologi</b> 
 * in January 2006.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *  o Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer. 
 *     
 *  o Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution. 
 *     
 *  o Neither the name of Flamingo Kirill Grouchnikov Topologi nor the names of 
 *    its contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission. 
 *     
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.jvnet.flamingo.bcb;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import javax.swing.event.EventListenerList;

/**
 * Model for the breadcrumb bar component ({@link JBreadcrumbBar}).
 * 
 * @param <T>
 *            Type of data associated with each breadcrumb bar item.
 * @author Kirill Grouchnikov
 */
public class BreadcrumbBarModel<T> {
	/**
	 * The list of breadcrumb items.
	 */
	private LinkedList<BreadcrumbItem<T>> items;

	/**
	 * Listener list.
	 */
	protected EventListenerList listenerList;

	/**
	 * Indication whether the model is in cumulative mode.
	 * 
	 * @see #setCumulative(boolean)
	 */
	protected boolean isCumulative;

	/**
	 * Smallest index of path change since the last call to
	 * {@link #setCumulative(boolean)} with <code>true</code>.
	 */
	protected int smallestCumulativeIndex;

	/**
	 * Creates a new empty model.
	 */
	public BreadcrumbBarModel() {
		this.items = new LinkedList<BreadcrumbItem<T>>();
		this.listenerList = new EventListenerList();
		this.isCumulative = false;
		this.smallestCumulativeIndex = -1;
	}

	/**
	 * Returns the index of the specified item.
	 * 
	 * @param item
	 *            Item.
	 * @return Index of the item if it is in the model or -1 if it is not.
	 */
	public int indexOf(BreadcrumbItem<T> item) {
		return this.items.indexOf(item);
	}

	/**
	 * Removes the last item in this model.
	 */
	public void removeLast() {
		this.items.removeLast();
		this.firePathChanged(this.items.size());
	}

	/**
	 * Resets this model, removing all the items.
	 */
	public void reset() {
		this.items.clear();
		this.firePathChanged(0);
	}

	/**
	 * Returns an unmodifiable list of the items in this model.
	 * 
	 * @return Unmodifiable list of the items in this model.
	 */
	public List<BreadcrumbItem<T>> getItems() {
		return Collections.unmodifiableList(this.items);
	}

	/**
	 * Returns the number of items in this model.
	 * 
	 * @return Number of items in this model.
	 */
	public int getItemCount() {
		return this.items.size();
	}

	/**
	 * Returns the model item at the specified index.
	 * 
	 * @param index
	 *            Item index.
	 * @return The model item at the specified index. Will return
	 *         <code>null</code> if the index is negative or larger than the
	 *         number of items.
	 */
	public BreadcrumbItem<T> getItem(int index) {
		if (index < 0)
			return null;
		if (index >= this.getItemCount())
			return null;
		return this.items.get(index);
	}

	/**
	 * Replaces the current item list with the specified list.
	 * 
	 * @param items
	 *            New contents of the model.
	 */
	public void replace(List<BreadcrumbItem<T>> items) {
		this.items.clear();
		for (int i = 0; i < items.size(); i++) {
			this.items.addLast(items.get(i));
		}
		this.firePathChanged(0);
	}

	/**
	 * Adds the specified item at the end of the path.
	 * 
	 * @param item
	 *            Item to add.
	 */
	public void addLast(BreadcrumbItem<T> item) {
		this.items.addLast(item);
		this.firePathChanged(this.items.size() - 1);
	}

	/**
	 * Starts or ends the cumulative mode. In cumulative mode calls to
	 * {@link #addLast(BreadcrumbItem)}, {@link #removeLast()},
	 * {@link #replace(List)} and {@link #reset()} will not fire events on the
	 * listeners registered with
	 * {@link #addPathListener(BreadcrumbPathListener)}.
	 * 
	 * @param isCumulative
	 *            If <code>true</code>, the model enters cumulative mode. If
	 *            <code>false</code>, the model exist cumulative mode and fires
	 *            a path event on all registered listeners with the smallest
	 *            index of all changes that have happened since the last time
	 *            this method was called with <code>true</code>.
	 */
	public void setCumulative(boolean isCumulative) {
		boolean toFire = this.isCumulative && !isCumulative;
		this.isCumulative = isCumulative;

		if (toFire) {
			this.firePathChanged(Math.max(0, this.smallestCumulativeIndex));
			this.smallestCumulativeIndex = -1;
		}
	}

	/**
	 * Adds the specified path listener to this model.
	 * 
	 * @param l
	 *            Path listener to add.
	 */
	public void addPathListener(BreadcrumbPathListener l) {
		this.listenerList.add(BreadcrumbPathListener.class, l);
	}

	/**
	 * Removes the specified path listener from this model.
	 * 
	 * @param l
	 *            Path listener to remove.
	 */
	public void removePathListener(BreadcrumbPathListener l) {
		this.listenerList.remove(BreadcrumbPathListener.class, l);
	}

	/**
	 * Fires a {@link BreadcrumbPathEvent}.
	 * 
	 * @param indexOfFirstChange
	 *            Index of the first item that has changed in the model.
	 */
	protected void firePathChanged(int indexOfFirstChange) {
		if (this.isCumulative) {
			if (this.smallestCumulativeIndex == -1)
				this.smallestCumulativeIndex = indexOfFirstChange;
			else
				this.smallestCumulativeIndex = Math.min(
						this.smallestCumulativeIndex, indexOfFirstChange);
			return;
		}
		BreadcrumbPathEvent event = new BreadcrumbPathEvent(this,
				indexOfFirstChange);
		// Guaranteed to return a non-null array
		Object[] listeners = this.listenerList.getListenerList();
		// Process the listeners last to first, notifying
		// those that are interested in this event
		for (int i = listeners.length - 2; i >= 0; i -= 2) {
			if (listeners[i] == BreadcrumbPathListener.class) {
				((BreadcrumbPathListener) listeners[i + 1])
						.breadcrumbPathEvent(event);
			}
		}
	}

}
